package com.hup.utils.io.batch;

import com.hup.utils.commons.DateBuilder;
import com.hup.utils.commons.TimerPicker;
import com.hup.utils.commons.hardware.SystemUtil;
import com.hup.utils.commons.thread.ThreadPoolUtil;
import com.hup.utils.commons.thread.ThreadUtil;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 批处理执行器
 *
 * @author hugan
 * @date 2020/1/21
 */
@Log4j2
public class BatchUtil {

    /**
     * 用于执行命令时,异步读取数据[运行结果,错误信息]
     */
    @Getter(lazy = true)
    private static final ThreadPoolExecutor EXECUTOR = ThreadPoolUtil.fixedThreadExecutor("BatchUtil-", 2, "批处理执行器");
    /**
     * 批处理执行时,的最大等待时间ms
     */
    private static final long MAX_WAIT_TIME = 10000;
    /**
     * 超时信息标识
     */
    public static final String TIMEOUT_TAG = "指令未结束,已超时";

    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public static class BatchBuilder {
        /**
         * 只有在exec前才BatchUtil.getEXECUTOR():避免仅使用外部执行器时,也初始化了省缺执行器
         */
        ThreadPoolExecutor executor;
        Long maxWaitTime = BatchUtil.MAX_WAIT_TIME;

        public BatchBuilder executor(ThreadPoolExecutor executor) {
            this.executor = executor;
            return this;
        }

        public BatchBuilder maxWaitTime(Long maxWaitTime) {
            this.maxWaitTime = maxWaitTime;
            return this;
        }

        /**
         * {@link #exec(String, BaseBatch, Long, ThreadPoolExecutor)}
         */
        public <R> R exec(BaseBatch<R> baseBatch) {
            if (executor == null) executor = BatchUtil.getEXECUTOR();
            ArrayList<String> respLine = BatchUtil.exec(baseBatch.cmd(), baseBatch, maxWaitTime, executor);
            return baseBatch.parse(respLine);
        }

        /**
         * {@link #exec(String, BaseBatch, Long, ThreadPoolExecutor)}
         */
        public ArrayList<String> exec(String cmd) {
            if (executor == null) executor = BatchUtil.getEXECUTOR();
            return BatchUtil.exec(cmd, null, maxWaitTime, executor);
        }
    }

    /**
     * @return 当前程序是否由管理员权限
     */
    public static boolean isAdmin() {
        return SystemUtil.isAdmin();
    }

    public static BatchBuilder builder() {
        return new BatchBuilder();
    }

    /**
     * @param baseBatch nullable
     * @param executor 异步执行器,用于异步读取结果流
     * @apiNote 请使用builder调用!此方法是线程安全的,多线程调用时process之间无相互影响,但受executor的限制!
     * @return 批处理执行结果;如果命令执行超时,则会在最后一行有 {@link #TIMEOUT_TAG}
     */
    private static ArrayList<String> exec(String cmd, BaseBatch<?> baseBatch, Long maxWaitTime,
            ThreadPoolExecutor executor) {
        TimerPicker.markStart();
        Process process = null;
        try {
            Runtime rt = Runtime.getRuntime();
            log.debug("cmd={}", cmd);
            process = rt.exec(cmd);
            log.debug("exec={}", TimerPicker.getStep());

            /*
                看skipReadStream注释!!:很大的坑
                命令会回复大量数据时,必须先异步读取数据流,再调用process.waitFor(),
                    否则process.waitFor()会一直阻塞
             */
            if (baseBatch != null && baseBatch.skipReadStream()) {
                log.debug("waitFor.st={}", TimerPicker.getStep());
                if (maxWaitTime != null) {
                    process.waitFor(maxWaitTime, TimeUnit.MILLISECONDS);
                } else {
                    process.waitFor();
                }
                log.debug("waitFor.ed={}", TimerPicker.getStep());
                return new ArrayList<>();
            }

            IpsRunnable[] resRunnable = new IpsRunnable[]{
                    new IpsRunnable(true, process),
                    new IpsRunnable(false, process)
            };
            for (IpsRunnable ipsRunnable : resRunnable) {
                executor.execute(ipsRunnable);
            }

            log.debug("waitFor.st={}", TimerPicker.getStep());
            String timeOutErr = null;
            if (maxWaitTime != null) {
                //n秒内执行完的会马上返回,最多等待n秒
                process.waitFor(maxWaitTime, TimeUnit.MILLISECONDS);
                if (process.isAlive()) {
                    timeOutErr = TIMEOUT_TAG + "(" + DateBuilder.spanChar(maxWaitTime) + ")";
                    log.info(timeOutErr);

                 /*
                 这是实现超时打断的关键代码:打断inputStream.read
                    只有destroyForcibly有效
                    Thread.interrupt是无效的
                测试情况:
                    没观察到destroyForcibly后还有流数据日志输出,
                        如果有,那下面的方法就会有异常了,list同时被多线程访问?
                 */
                    process.destroyForcibly();
                }
            } else {
                process.waitFor();
            }
            log.debug("waitFor.ed={}", TimerPicker.getStep());

            int activeCount;
            while ((activeCount = executor.getActiveCount()) != 0) {
                log.warn("数据流未结束.count={}", activeCount);
                /*
                即使检测到执行器未结束,也没必要执行等待或其他操作了,因为process已终止,异步线程会在极短暂的时间内就结束
                这句话可能非绝对???生产中存在极小机率出错
                 */
                ThreadUtil.sleep(200);//临时的非正式处理方式
            }

            //返回数据:如果都有数据,结果是[成功信息]+[错误信息]
            resRunnable[0].lstRes.addAll(resRunnable[1].lstRes);
            //如果是超时,则在第一行添加超时信息
            if (timeOutErr != null) resRunnable[0].lstRes.add(timeOutErr);
            return resRunnable[0].lstRes;
        } catch (Exception e) {
            /*
            出现情况:
            1.命令不存在:[如:ping1 abc]
             */
            throw new BatExecException(cmd, e, baseBatch);
        } finally {
            if (process != null) {
                /*
                ping命令现象:运行时,任务管理器会有[PING]的进程
                    process.waitFor前执行完,则destroy前[PING]进程就关闭了
                    process.destroy前没执行完,则destroy后[PING]进程也会马上关闭
                观察结论:
                    其他指令可能也是一样时,系统资源是没有泄露的,能及时停止没结束的任务
                 */
                process.destroyForcibly();
            }
        }
    }

    private static class IpsRunnable implements Runnable {

        private final boolean isRes;
        private final Process process;
        private ArrayList<String> lstRes;

        public IpsRunnable(boolean isRes, Process process) {
            this.isRes = isRes;
            this.process = process;
            lstRes = new ArrayList<>();
        }

        @Override
        public void run() {
            //中国系统编码GBK
            try (InputStream ips = isRes ? process.getInputStream() : process.getErrorStream();
                 BufferedReader reader = new BufferedReader(new InputStreamReader(ips, Charset.forName("GBK")))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    lstRes.add(line);
                    log.debug("{}, line={}", isRes ? "res" : "err", line);
                }
            } catch (Exception e) {
                log.warn("isRes={}", isRes, e);
                lstRes.add(e.getMessage());
            }
            /*
            线程末尾,不应该有耗时代码,与另一个地方遇到的是一样的问题
            即使是log,如果有io操作,就会耗时,导致process.waitFor()结束后executor.getActiveCount()不为0
             */
            //log.debug("finished, isRes={}", isRes);
        }
    }

}
